/*
 * ﻿Copyright (C) 2011-2013 NewMain Softech
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package com.newmainsoftech.aspectjutil.dynamicagent;

import java.io.File;
import java.lang.instrument.Instrumentation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.Attributes;
import java.util.jar.Attributes.Name;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.logging.Logger;

public class AgentAssistant {
	Logger logger = Logger.getLogger( this.getClass().getName());
		protected Logger getLogger() {
			return logger;
		}

	static class ClassNameComparator implements Comparator<Class> {
		public int compare( String className1, String className2) {
			if ( className1 == className2) return 0;
			if ( ( className1 == null) && ( className2 != null)) return -1;
			if ( ( className1 != null) && ( className2 == null)) return 1;
			return className1.compareTo( className2);
		}
		@Override
		public int compare( Class class1, Class class2) {
			if ( class1 == class2) return 0;
			if ( ( class1 == null) && ( class2 != null)) return -1;
			if ( ( class1 != null) && ( class2 == null)) return 1;
			if ( class1.equals( class2)) return 0;
			String className1 = class1.getName(); 
			String className2 = class2.getName();
			return compare( className1, className2);
		}
	}
	
	/**
	 * Sort elements in <code>classList</code> input by class name.
	 * Not thread safe; no synchronization on <code>classList</code> input within this method. 
	 * @param classList
	 */
	public void sortByClassName( List<Class> classList) {
		ClassNameComparator classNameComparator = new ClassNameComparator();
		Collections.sort( classList, classNameComparator);
	}
	/**
	 * Find Class object of what name matches with value of <code>className</code> input 
	 * out of <code>classList</code> input.
	 * 
	 * @param className name of class to look for in <code>classList</code> input.
	 * @param classList List of Class objects being searched.
	 * @return Class of what name matches with value of <code>className</code> input when found,  
	 * otherwise null.
	 */
	public Class findClass( final String className, final List<Class> classList) {
		// Validation on inputs -------------------------------------------------------------------
		String classNameCopy = ((className == null) ? "" : className.trim());
			if ( classNameCopy.length() < 1) {
				throw new IllegalArgumentException(
						String.format(
								"Value of className input is not valid string as class name: %1$s",
								((className == null) ? "null" : ("\"" + className + "\""))
								)
						);
			}
		if ( classList == null) {
			throw new IllegalArgumentException( "Value of classList input cannot be null");
		}
		if ( classList.size() < 1) {
			return null;
		}
		// ----------------------------------------------------------------------------------------
		
		List<Class> classListCopy = new ArrayList<Class>( classList);
			ClassNameComparator classNameComparator = new ClassNameComparator();
			Collections.sort( classListCopy, classNameComparator);
		
		int gap = classListCopy.size()/2;
		if ( gap == 0) {
			if ( classNameCopy.equals( classListCopy.get( 0).getName())) {
				return classListCopy.get( 0);
			}
			return null;
		}
		else {
			int classIndex = 0;
			while( gap > 0) {
				classIndex = classIndex + gap;
				int comparisonResult 
				= classNameComparator.compare( classNameCopy, classListCopy.get( classIndex).getName());
				if ( comparisonResult == 0) {
					return classListCopy.get( classIndex);					
				}
				else if ( comparisonResult < 0) {
					classIndex = classIndex - gap;
				}
				gap = gap/2;
					if ( gap == 0) {
						if ( comparisonResult > 0) ++classIndex;
						if ( classNameCopy.equals( classListCopy.get( classIndex).getName())) {
							return classListCopy.get( classIndex);
						}
						if ( (classListCopy.size() % 2) == 1) {
							if ( classNameCopy.equals( classListCopy.get( ++classIndex).getName())) {
								return classListCopy.get( classIndex);
							}
						}
						return null;
					}
			} // while
			return null;
		}
	}
	
	/**
	 * Check whether Class of what name is specified by <code>className</code> input has 
	 * been loaded before and return the loaded Class object found first.
	 *  
	 * @param className Name of loaded class to look for.
	 * @param instrumentation being used to enumerate list of loaded classes.
	 * @return Class object found first. 
	 * null when cannot find Class object with same name specified by <code>className</code>.
	 */
	public Class<?> hasLoaded( final String className, final Instrumentation instrumentation) {
		String classNameCopy = ((className == null) ? "" : className.trim());
			if ( classNameCopy.length() < 1) {
				throw new IllegalArgumentException( 
						String.format(
								"Given value of className input is not valid as a class name: %1$s",
								((className == null) ? "null" : ("\"" + className + "\""))
								)
						);
			}
		if ( instrumentation == null) {
			throw new IllegalArgumentException( "Value of instrumentation input cannot be null.");
		}
		
		List<Class> loadedClassList = Arrays.asList( instrumentation.getAllLoadedClasses());
		return findClass( className, loadedClassList);
	}
	/**
	 * Check whether Class specified by <code>classObj</code> input has been loaded before. 
	 * 
	 * @param classObj Class object of subject.
	 * @param instrumentation being used to enumerate list of loaded classes. 
	 * @return true when Class specified by <code>classObj</code> has been already loaded. 
	 * false when cannot find loaded Class specified by <code>classObj</code>.
	 */
	public boolean hasLoaded( final Class<?> classObj, final Instrumentation instrumentation) {
		if ( classObj == null) {
			throw new IllegalArgumentException( "Value of classObj input cannot be null.");
		}
		if ( instrumentation == null) {
			throw new IllegalArgumentException( "Value of instrumentation input cannot be null.");
		}
		
		List<Class> loadedClassList = Arrays.asList( instrumentation.getAllLoadedClasses());
		int classIndex = loadedClassList.indexOf( classObj);
		if ( classIndex < 0) return false;
		return true;
	}
	/**
	 * Check whether Class specified by <code>classObj</code> input has been initiated by 
	 * class loader specified by <code>classLoader</code>. 
	 * 
	 * @param classObj Class object of subject.
	 * @param classLoader class loader object being checked for whether it's loader of 
	 * <code>classObj</code> input. null value implies bootstrap class loader.
	 * @param instrumentation being used to enumerate list of classes initiated 
	 * by <code>classLoader</code> input. 
	 * @return true  
	 */
	public boolean hasInitiatedBy( 
			final Class<?> classObj, final ClassLoader classLoader, final Instrumentation instrumentation) 
	{
		Class[] initiatedClassArray = instrumentation.getInitiatedClasses( classLoader);
		return Arrays.asList( initiatedClassArray).contains( classObj);
	}
	/**
	 * Check whether value given by <code>file</code> input is found in application's system class path.
	 * @param file must point existing place in file system. 
	 * @return true when value given by <code>file</code> input is found in application's system class path.
	 */
	public boolean isInSystemClassPath( File file) {
		if ( file == null) {
			throw new IllegalArgumentException( "Value of file input cannot be null");
		}
		if ( !file.exists()) {
			throw new IllegalArgumentException( 
					String.format(
							"%1$s points location not existing.",
							file.getPath()
							)
					);
		}
		
		Logger logger = getLogger();
		
		String systemClassPath = System.getProperty( "java.class.path");
		for ( String path : systemClassPath.split( File.pathSeparator)) {
			File pathFile = new File( path);
			if ( pathFile.exists()) {
				if ( pathFile.getAbsolutePath().equals( file.getAbsolutePath())) {
					if ( logger.isLoggable( Level.FINE)) {
						logger.log(
								Level.FINE,
								String.format(
										"%1$s is in application's class path.",
										file.getPath()
										)
								);
					}
					return true;
				}
			}
		}
		if ( logger.isLoggable( Level.FINE)) {
			logger.log(
					Level.FINE,
					String.format(
							"%1$s is not in application's class path.",
							file.getPath()
							)
					);
		}
		
		return false;
	}
	
	public void addAdditionalJarPathsToSystemClassPath(
			String[] additionalJarsPaths, boolean checkJarAddition, final Instrumentation instrumentation) 
	{
		Logger logger = getLogger();
		// Inputs' validation ---------------------------------------------------------------------
		if ( additionalJarsPaths == null) {
			throw new IllegalArgumentException( "Value of additionalJarsPaths input cannot be null.");
		}
		if ( instrumentation == null) {
			throw new IllegalArgumentException( "Value of instrumentation input cannot be null.");
		}
		// ----------------------------------------------------------------------------------------
	
		for( String jarPath : additionalJarsPaths) {
			File file = new File( jarPath);
			JarFile jarFile = null;
				try {
					jarFile = new JarFile( file);
				}
				catch( Exception exception) {
					if ( logger.isLoggable( Level.INFO)) {
						logger.log(
								Level.INFO,
								String.format(
										"Skipping to add %1$s to application's system class path since " 
										+ "it appeared as not accessible a .jar file",
										jarPath
										), 
								exception		
								);
					}
					continue; // for
				}
				
			// Check whether mockJarFile has been already loaded --------------------------------------
			if ( checkJarAddition) {
				String classToLoad = null;
					try {
						// Put main class name out of MANIFEST.MF to classToLoad
						Manifest manifest = jarFile.getManifest();
						Attributes mainAttributes = manifest.getMainAttributes();
						if ( mainAttributes != null) {
							classToLoad = mainAttributes.getValue( Name.MAIN_CLASS);
						}
					}
					catch( Exception exception) { // Ignore
					}
					if ( classToLoad == null) {
						// Put name of not-inner class found first to classToLoad 
						Enumeration<JarEntry> jarEntries = jarFile.entries();
						while( jarEntries.hasMoreElements()) {
							String jarEntryName = jarEntries.nextElement().getName();
							if ( jarEntryName.endsWith( ".class")) {
								if ( jarEntryName
										.substring( 0, jarEntryName.lastIndexOf( "/"))
										.contains( "$")) {
									continue; // while
								}
								classToLoad = jarEntryName.replaceAll( "/", ".");
								break; // while
							}
						} // while
					}
				if ( classToLoad == null) {
					if ( logger.isLoggable( Level.FINE)) {
						logger.log(
								Level.FINE,
								String.format(
										"%1$s seems to contain only resources but classes.", 
										jarPath
										)
								);
					}
					if ( isInSystemClassPath( file)) {
						continue; // for
					}
					// otherwise add jarfile to application's system class path
				}
				else {
					Class<?> loadedClass = null;
					String loadedArchivePath = null;
					try {
						loadedClass 
						= Class.forName( classToLoad, false, ClassLoader.getSystemClassLoader());
						File loadedArchiveFile 
						= new File( loadedClass.getProtectionDomain().getCodeSource().getLocation().toURI());
						loadedArchivePath = loadedArchiveFile.getAbsolutePath();
					}
					catch( Exception exception) { // ignore
					}
					if ( loadedClass != null) {
						if ( logger.isLoggable( Level.INFO)) {
							logger.info( 
									String.format(
											"Skipped adding %1$s to application's system class path since " 
											+ "%2$s class can be loaded from %3$s even before adding the " 
											+ "specified %1$s to application class path. " 
											+ "%nHere's dump of application system class path as refence " 
											+ "(although only system class path is not reliable source for " 
											+ "resolving class): %4$s",
											jarPath, 
											classToLoad,
											((loadedArchivePath == null) 
													? "archival place" : loadedArchivePath),
											System.getProperty( "java.class.path")
											)
									);
						}
						continue; // for
					}
				}
			}
			// ------------------------------------------------------------------------------------
			
			// Add mockJarFile to application's class path --------------------------------------------
			try {
				instrumentation.appendToSystemClassLoaderSearch( jarFile);
				if ( logger.isLoggable( Level.FINE)) {
					logger.log( 
							Level.FINE, 
							String.format(
									"Succeeded to add %1$s to application's system class path.",
									jarPath
									));
				}
			}
			catch( Exception exception) {
				if ( logger.isLoggable( Level.SEVERE)) {
					logger.log( 
							Level.SEVERE, 
							String.format(
									"Could not add %1$s to application's system class path.",
									jarPath
									),
							exception
							);
				}
			}
			// ------------------------------------------------------------------------------------
		} // for
	}
}
